<?php
namespace heihei\security;
use yii;
use yii\base\Component;
use yii\base\InvalidConfigException;
use yii\base\InvalidValueException;
use yii\base\InvalidParamException;
use yii\base\Security;

use yii\helpers\StringHelper;

use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Parser;

/**
 * JWT 加密
 *
 * iss  #非必须  issuer 请求实体，可以是发起请求的用户的信息，也可是jwt的签发者。如：http://example.org 
 * aud  #非必须  接收该JWT的一方。如：http://example.com
 * iat  #非必须  issued at。 令牌创建时间，unix时间戳格式。如：1356999524
 * exp  #非必须  expire 指定令牌的生命周期。unix时间戳格式。如：1548333419 
 * sub  #非必须  该JWT所面向的用户。如：test@example.com
 * nbf  #非必须  not before。如果当前时间在nbf里的时间之前，则Token不被接受；一般都会留一些余地，比如几分钟。如：1357000000
 * jti  #非必须  JWT ID。针对当前token的唯一标识。如：5d83409a0b47b
 * uid  #非必须  登录用户ID。如：888
 */
class Jwt extends Component
{    
    /**
     * @var 秘钥
     */
    public $key;

    /**
     * @var 签名算法
     *      支持的算法有'HS256'、'HS384'、'HS512'和'RS256'
     */
    public $alg = 'HS256'; 


    public function decode($jwtTokenCode){
        if (!$this->key) {
            throw new InvalidConfigException(get_class($this) . '::key 必须配置秘钥');
        }
        
        $token = (new Parser())->parse($jwtTokenCode);

        if($token->isExpired()){
            throw new InvalidValueException("JWT令牌已经过期！", 403.13);
        }

        $signer = $this->getSigner();
        $verifyResult = $token->verify($signer, $this->key);
        if(!$verifyResult){
            throw new InvalidValueException("JWT令牌验证失败！", 403.14);
        }
        
        $claimArr = [];
        foreach ($token->getClaims() as $claimKey => $claim) {
            $claimArr[$claimKey] = $claim->getValue();
        }
        if(isset($claimArr['userDataCipher']) && !empty($claimArr['userDataCipher'])){
            $userData = $this->dataDecode($claimArr['userDataCipher']);
            $claimArr = array_merge($claimArr, $userData);
        }
        return (object)$claimArr;
    }

    protected function dataDecode($userDataCipher){
        $decryptResult = (new Security())->decryptByKey(StringHelper::base64UrlDecode($userDataCipher), $this->key);
        return json_decode($decryptResult, true);
    }

    /**
     * JWT 加密
     *
     * @param object|array  $data   待加密的数据
     *
     * @return 返回JWT 加密字符串
     */
    public function encode($data, $duration = 86400)
    {   
        
        if(empty($data)){
            throw new InvalidParamException('data参数不能为空！');
        }

        if (empty($this->key)) {
            throw new InvalidConfigException(get_class($this) . '::key 必须配置秘钥');
        }

        
        $time = time();
        $builder = (new Builder())
            //->issuedBy('http://example.com') // Configures the issuer (iss claim)
            //->permittedFor('http://example.org') // Configures the audience (aud claim)
            ->identifiedBy(uniqid()) // Configures the id (jti claim), replicating as a header item
            ->issuedAt((new \DateTimeImmutable())->setTimestamp($time)) // 配置令牌发放时间(iat claim)。
            ->canOnlyBeUsedAfter((new \DateTimeImmutable())->setTimestamp($time + 60)) // 配置令牌可使用的时间 (nbf claim)
            ->expiresAt((new \DateTimeImmutable())->setTimestamp($time + $duration)); // 配置令牌的过期时间 (exp claim)

        $encryptResult = $this->dataEncode($data);
        $builder->withClaim('userDataCipher', $encryptResult); // 配置自定义参数 "uid"

        // 返回生成的令牌
        $signer = $this->getSigner();
        $token = $builder->getToken($signer, new Key($this->key));

        return (string)$token;
    }

    protected function dataEncode($data){
        $dataStr = is_array($data) ? json_encode($data) : $data;
        $encryptResult = (new Security())->encryptByKey($dataStr, $this->key);
        return StringHelper::base64UrlEncode($encryptResult);
    }

    private function getSigner(){
        $signType = strtolower(substr($this->alg, 0, 2));
        if(strtolower($signType)  == 'hs'){
            $signType = 'Hmac';
        }
        else{
            $signType = 'Rsa';
        }
        $signWay = 'Sha' . substr($this->alg, 2, 3);
        
        $signerClassName = 'Lcobucci\JWT\Signer' . '\\' . $signType . '\\' . $signWay;
        return new $signerClassName;
    }
}
